/**
 * @author Steve Sedlmayr
 * @mail ssedlmayr@gmail.com
 * @link http://code.google.com/p/chitin/
 * @version 0.1
 
	The MIT License

	Copyright (c) 2009 Steve Sedlmayr

	Permission is hereby granted, free of charge, to any person obtaining a copy
	of this software and associated documentation files (the "Software"), to deal
	in the Software without restriction, including without limitation the rights
	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	copies of the Software, and to permit persons to whom the Software is
	furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in
	all copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
	THE SOFTWARE.
*/

package srs.chitin.mvstate.base
{	
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IEventDispatcher;
	import flash.utils.*;
	
	/** 
	 * Event for notification of changes to this model from external clients.
	 * 
	 * @eventType srs.chitin.mvstate.base.InboundEventBase.INBOUND_EVENT
	 */
	[Event(name="inboundEvent", type="InboundEventBase")]
	
	/** 
	 * Event for outbound notification of changes to this model to listeners of the model.
	 * 
	 * @eventType srs.chitin.mvstate.base.OutboundEventBase.OUTBOUND_EVENT
	 */
	[Event(name="outboundEvent", type="OutboundEventBase")]
	
	/** 
	 * Base class for the Model portion of the truncated MVC pattern.
	 * A subclassable Singleton.
	 */
	public class ModelBase implements IModel, IEventDispatcher {
		/**
		 * The Singleton instance of the ModelBase class.
		 */
		private static var _instance:ModelBase;
		
		/** 
		 * A value object to be initialized with fields on the data model at runtime.
		 */
		protected var _notificationVO:Object = new Object();
		
		/** 
		 * Returns a clone of the current VO.
		 *
		 * @return A clone of the current VO.
		 */
		public function get notificationVO() : Object {
			var clone:Object = new Object;
			
			for ( var property:String in this._notificationVO ) {
				clone[property] = this._notificationVO[property];
			}
			
			return clone;
		}
		
		/** 
		 * An enumeration of custom events, if any, defined in a subclass.
		 */
		[ArrayElementType("String")]
		protected var _customEventsArray:Array;
		
		/** 
		 * An enumeration of fields to filter on outbound events, if any, as defined in a subclass.
		 */
		[ArrayElementType("String")]
		protected var _outboundChangeFilter:Array;
		
		/** 
		 * An EventDispatcher object. We implement IEventDispatcher instead of extending EventDispatcher to
		 * avoid a hard-coded dependency on the EventDispatcher class.
		 */
		protected var _eventDispatcher:EventDispatcher = new EventDispatcher(this as IEventDispatcher);
		
		/**
		 * ModelBase class constructor. Adds an internal listener for inbound events.
		 * Throws an error if a client class tries to instantiate the class directly.
		 * 
		 * @param enforcer An enforcer class for the Singleton pattern in the
		 * Model class. Each layer of hierarchy from ModelBase should have a corresponding
		 * enforcer class derived from ModelBaseEnforcer with the same degree of inheritance.
		 */
		public function ModelBase(enforcer:ModelBaseEnforcer=null) : void {
			if ( enforcer != null ) {
				//Add an internal listener for inbound notifications
				this.addEventListener(InboundEventBase.INBOUND_EVENT, this.inboundEventHandler, false, 0, true);
			} else {
				//TO-DO :: get the id param working
				//Throw an error if someone attempts to instantiate directly via the constructor
				throw new Error("PATTERN ERROR: YOU HAVE ATTEMPTED ILLEGAL INSTANTIATION OF A SINGLETON CLASS." + 
					"THIS CONSTRUCTOR REQUIRES A SUBCLASS OF ModelBaseEnforcer DEFINED AS A LOCAL GENERIC IN THE SINGLETON SUBCLASS." + 
					"PLEASE REFER TO YOUR PROJECT DOCUMENTATION AND CONTACT THE UI LEAD AND/OR AUTHOR OF THIS DOCUMENT.");
			}
		}
		
		/**
		 * Returns the Singleton instance for the ModelBase class.
		 * 
		 * @return the Singleton instance for the ModelBase class.
		 */
		public static function getInstance() : ModelBase {
			if ( _instance == null ) {
				_instance = new ModelBase(new ModelBaseEnforcer());
				_instance.addCustomEvents(new Array());
			}
			
			return _instance;
		}
		
		/** 
		 * Populate the enumeration of custom events defined by metadata in a subclass.
		 * 
		 * @param events An array of custom events.
	 	 */
		protected function addCustomEvents(events:Array) : void {
			getDefinitionByName(getQualifiedClassName(this)).getInstance()._customEventsArray = events;
		}
		
		/** 
		 * Returns the enumeration of custom events defined by metadata in a subclass.
		 * 
		 * @return An array of custom events defined by metadata in a subclass.
		 */
		public function getCustomEvents() : Array { return getDefinitionByName(getQualifiedClassName(this)).getInstance()._customEventsArray; }
		
		/** 
		 * An internal handler for notifications of model changes from external clients.
		 * 
		 * @param event The inbound event to be handled from a client class.
		 */
		protected function inboundEventHandler(event:InboundEventBase) : void {
			if ( event.inboundNotificationVO != null ) {
				var changed:Array = new Array();
				var inboundNotificationVO:ModelBaseVO = event.inboundNotificationVO;
				var inboundNotificationVOEnumeration:Array = inboundNotificationVO.getEnumeration();
				var localNotificationVO:Object = this._notificationVO;
				var outboundEvent:OutboundEventBase = new OutboundEventBase(OutboundEventBase.OUTBOUND_EVENT);
					outboundEvent.outboundNotificationVO = inboundNotificationVO;
				var hasChanged:Boolean = false;
				
				/* Compare the internal field object to the incoming VO;
				   If anything differs, store it in the local data model.
				   We populate this._notificationVO in a lazy fashion. */
				for ( var i:Number = 0; i < inboundNotificationVOEnumeration.length; i++ ) {
					if ( localNotificationVO[inboundNotificationVOEnumeration[i]] == undefined || 
						localNotificationVO[inboundNotificationVOEnumeration[i]] != 
							inboundNotificationVO.getPropertyByName(inboundNotificationVOEnumeration[i]) ) {
						//Update the local VO with changes
						localNotificationVO[inboundNotificationVOEnumeration[i]] = 
							inboundNotificationVO.getPropertyByName(inboundNotificationVOEnumeration[i]);
						
						//If an outbound change filter is present, set the filter flag
						var filter:Boolean = false;
						
						if ( this._outboundChangeFilter != null && this._outboundChangeFilter.length > 0 ) {
							for ( var k:Number = 0; k < this._outboundChangeFilter.length; k++ ) {
								if ( this._outboundChangeFilter[k] == inboundNotificationVOEnumeration[i] ) {
									filter = true;
									break;
								}
							}
						}
						
						//If the current field isn't filtered, add it to the changed Array
						if ( filter == false ) { changed.push(inboundNotificationVOEnumeration[i]); }
						
						hasChanged = true;
					}
				}
				
				/* If the forcedChangeNotifications property is present, set
				   the hasChanged flag to true and push its items into the changed 
				   Array for outbound notifications. */
				if ( event.forcedChangeNotifications != null && event.forcedChangeNotifications.length > 0 ) {
					hasChanged = true;
					
					for ( var j:Number = 0; j < event.forcedChangeNotifications.length; j++ ) {
						//Only push to changed array if it is not already there.
						if ( changed.indexOf(event.forcedChangeNotifications[j]) < 0 ) {
							changed.push(event.forcedChangeNotifications[j]);
						}
					}
				}
				
				//If the data model has changed, notify listeners
				if ( hasChanged == false ) { return; }
				
				outboundEvent.changed = changed;
				dispatchEvent(outboundEvent);
			}
		}
		
		/* IEventDispatcher methods */
		
		/**
		 * @see http://livedocs.adobe.com/flash/9.0/ActionScriptLangRefV3/flash/events/EventDispatcher.html
		 */
		public function addEventListener(type:String, listener:Function, useCapture:Boolean = false, priority:int = 0, useWeakReference:Boolean = false) : void {
			_eventDispatcher.addEventListener(type, listener, useCapture, priority, useWeakReference);
		}
 	 	
		/**
		 * @see http://livedocs.adobe.com/flash/9.0/ActionScriptLangRefV3/flash/events/EventDispatcher.html
		 */
		public function dispatchEvent(event:Event) : Boolean {
			return _eventDispatcher.dispatchEvent(event);
		}
		 	 	
		/**
		 * @see http://livedocs.adobe.com/flash/9.0/ActionScriptLangRefV3/flash/events/EventDispatcher.html
		 */
		public function hasEventListener(type:String) : Boolean {
			return _eventDispatcher.hasEventListener(type);
		}
		 	 	
		/**
		 * @see http://livedocs.adobe.com/flash/9.0/ActionScriptLangRefV3/flash/events/EventDispatcher.html
		 */
		public function removeEventListener(type:String, listener:Function, useCapture:Boolean = false) : void {
			_eventDispatcher.removeEventListener(type, listener, useCapture);
		}
		 	 	
		/**
		 * @see http://livedocs.adobe.com/flash/9.0/ActionScriptLangRefV3/flash/events/EventDispatcher.html
		 */
		public function willTrigger(type:String) : Boolean {
			return _eventDispatcher.willTrigger(type);
		}
	}
}